/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const {EventEmitter} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {actionCreators: ac, actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
const {getDefaultOptions} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm");

ChromeUtils.defineModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");

/*
 * Generators for built in sections, keyed by the pref name for their feed.
 * Built in sections may depend on options stored as serialised JSON in the pref
 * `${feed_pref_name}.options`.
 */
const BUILT_IN_SECTIONS = {
  "feeds.section.topstories": options => ({
    id: "topstories",
    pref: {
      titleString: {id: "header_recommended_by", values: {provider: options.provider_name}},
      descString: {id: "prefs_topstories_description2"},
      nestedPrefs: options.show_spocs ? [{
        name: "showSponsored",
        titleString: "prefs_topstories_options_sponsored_label",
        icon: "icon-info",
      }] : [],
    },
    shouldHidePref: options.hidden,
    eventSource: "TOP_STORIES",
    icon: options.provider_icon,
    title: {id: "header_recommended_by", values: {provider: options.provider_name}},
    learnMore: {
      link: {
        href: "https://getpocket.com/firefox/new_tab_learn_more",
        id: "pocket_how_it_works",
      },
    },
    privacyNoticeURL: "https://www.mozilla.org/privacy/firefox/#suggest-relevant-content",
    compactCards: false,
    rowsPref: "section.topstories.rows",
    maxRows: 4,
    availableLinkMenuOptions: ["CheckBookmarkOrArchive", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
    emptyState: {
      message: {id: "topstories_empty_state", values: {provider: options.provider_name}},
      icon: "check",
    },
    shouldSendImpressionStats: true,
    dedupeFrom: ["highlights"],
  }),
  "feeds.section.highlights": options => ({
    id: "highlights",
    pref: {
      titleString: {id: "settings_pane_highlights_header"},
      descString: {id: "prefs_highlights_description"},
      nestedPrefs: [{
        name: "section.highlights.includeVisited",
        titleString: "prefs_highlights_options_visited_label",
      }, {
        name: "section.highlights.includeBookmarks",
        titleString: "settings_pane_highlights_options_bookmarks",
      }, {
        name: "section.highlights.includeDownloads",
        titleString: "prefs_highlights_options_download_label",
      }, {
        name: "section.highlights.includePocket",
        titleString: "prefs_highlights_options_pocket_label",
      }],
    },
    shouldHidePref:  false,
    eventSource: "HIGHLIGHTS",
    icon: "highlights",
    title: {id: "header_highlights"},
    compactCards: true,
    rowsPref: "section.highlights.rows",
    maxRows: 4,
    emptyState: {
      message: {id: "highlights_empty_state"},
      icon: "highlights",
    },
    shouldSendImpressionStats: false,
  }),
};

const SectionsManager = {
  ACTIONS_TO_PROXY: ["WEBEXT_CLICK", "WEBEXT_DISMISS"],
  CONTEXT_MENU_PREFS: {"CheckSavedToPocket": "extensions.pocket.enabled"},
  CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES: {
    history: ["CheckBookmark", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
    bookmark: ["CheckBookmark", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
    pocket: ["ArchiveFromPocket", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
    download: ["OpenFile", "ShowFile", "Separator", "GoToDownloadPage", "CopyDownloadLink", "Separator", "RemoveDownload", "BlockUrl"],
  },
  initialized: false,
  sections: new Map(),
  async init(prefs = {}, storage) {
    this._storage = storage;

    for (const feedPrefName of Object.keys(BUILT_IN_SECTIONS)) {
      const optionsPrefName = `${feedPrefName}.options`;
      await this.addBuiltInSection(feedPrefName, prefs[optionsPrefName]);

      this._dedupeConfiguration = [];
      this.sections.forEach(section => {
        if (section.dedupeFrom) {
          this._dedupeConfiguration.push({
            id: section.id,
            dedupeFrom: section.dedupeFrom,
          });
        }
      });
    }

    Object.keys(this.CONTEXT_MENU_PREFS).forEach(k =>
      Services.prefs.addObserver(this.CONTEXT_MENU_PREFS[k], this));

    this.initialized = true;
    this.emit(this.INIT);
  },
  observe(subject, topic, data) {
    switch (topic) {
      case "nsPref:changed":
        for (const pref of Object.keys(this.CONTEXT_MENU_PREFS)) {
          if (data === this.CONTEXT_MENU_PREFS[pref]) {
            this.updateSections();
          }
        }
        break;
    }
  },
  updateSectionPrefs(id, collapsed) {
    const section = this.sections.get(id);
    if (!section) {
      return;
    }

    const updatedSection = Object.assign({}, section, {pref: Object.assign({}, section.pref, collapsed)});
    this.updateSection(id, updatedSection, true);
  },
  async addBuiltInSection(feedPrefName, optionsPrefValue = "{}") {
    let options;
    let storedPrefs;
    try {
      options = JSON.parse(optionsPrefValue);
    } catch (e) {
      options = {};
      Cu.reportError(`Problem parsing options pref for ${feedPrefName}`);
    }
    try {
      storedPrefs = await this._storage.get(feedPrefName) || {};
    } catch (e) {
      storedPrefs = {};
      Cu.reportError(`Problem getting stored prefs for ${feedPrefName}`);
    }
    const defaultSection = BUILT_IN_SECTIONS[feedPrefName](options);
    const section = Object.assign({}, defaultSection, {pref: Object.assign({}, defaultSection.pref, getDefaultOptions(storedPrefs))});
    section.pref.feed = feedPrefName;
    this.addSection(section.id, Object.assign(section, {options}));
  },
  addSection(id, options) {
    this.updateLinkMenuOptions(options, id);
    this.sections.set(id, options);
    this.emit(this.ADD_SECTION, id, options);
  },
  removeSection(id) {
    this.emit(this.REMOVE_SECTION, id);
    this.sections.delete(id);
  },
  enableSection(id) {
    this.updateSection(id, {enabled: true}, true);
    this.emit(this.ENABLE_SECTION, id);
  },
  disableSection(id) {
    this.updateSection(id, {enabled: false, rows: [], initialized: false}, true);
    this.emit(this.DISABLE_SECTION, id);
  },
  updateSections() {
    this.sections.forEach((section, id) => this.updateSection(id, section, true));
  },
  updateSection(id, options, shouldBroadcast) {
    this.updateLinkMenuOptions(options, id);
    if (this.sections.has(id)) {
      const optionsWithDedupe = Object.assign({}, options, {dedupeConfigurations: this._dedupeConfiguration});
      this.sections.set(id, Object.assign(this.sections.get(id), options));
      this.emit(this.UPDATE_SECTION, id, optionsWithDedupe, shouldBroadcast);
    }
  },

  /**
   * Save metadata to places db and add a visit for that URL.
   */
  updateBookmarkMetadata({url}) {
    this.sections.forEach((section, id) => {
      if (id === "highlights") {
        // Skip Highlights cards, we already have that metadata.
        return;
      }
      if (section.rows) {
        section.rows.forEach(card => {
          if (card.url === url && card.description && card.title && card.image) {
            PlacesUtils.history.update({
              url: card.url,
              title: card.title,
              description: card.description,
              previewImageURL: card.image,
            });
            // Highlights query skips bookmarks with no visits.
            PlacesUtils.history.insert({
              url,
              title: card.title,
              visits: [{}],
            });
          }
        });
      }
    });
  },

  /**
   * Sets the section's context menu options. These are all available context menu
   * options minus the ones that are tied to a pref (see CONTEXT_MENU_PREFS) set
   * to false.
   *
   * @param options section options
   * @param id      section ID
   */
  updateLinkMenuOptions(options, id) {
    if (options.availableLinkMenuOptions) {
      options.contextMenuOptions = options.availableLinkMenuOptions.filter(
        o => !this.CONTEXT_MENU_PREFS[o] || Services.prefs.getBoolPref(this.CONTEXT_MENU_PREFS[o]));
    }

    // Once we have rows, we can give each card it's own context menu based on it's type.
    // We only want to do this for highlights because those have different data types.
    // All other sections (built by the web extension API) will have the same context menu per section
    if (options.rows && id === "highlights") {
      this._addCardTypeLinkMenuOptions(options.rows);
    }
  },

  /**
   * Sets each card in highlights' context menu options based on the card's type.
   * (See types.js for a list of types)
   *
   * @param rows section rows containing a type for each card
   */
  _addCardTypeLinkMenuOptions(rows) {
    for (let card of rows) {
      if (!this.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES[card.type]) {
        Cu.reportError(`No context menu for highlight type ${card.type} is configured`);
      } else {
        card.contextMenuOptions = this.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES[card.type];

        // Remove any options that shouldn't be there based on CONTEXT_MENU_PREFS.
        // For example: If the Pocket extension is disabled, we should remove the CheckSavedToPocket option
        // for each card that has it
        card.contextMenuOptions = card.contextMenuOptions.filter(
          o => !this.CONTEXT_MENU_PREFS[o] || Services.prefs.getBoolPref(this.CONTEXT_MENU_PREFS[o]));
      }
    }
  },

  /**
   * Update a specific section card by its url. This allows an action to be
   * broadcast to all existing pages to update a specific card without having to
   * also force-update the rest of the section's cards and state on those pages.
   *
   * @param id              The id of the section with the card to be updated
   * @param url             The url of the card to update
   * @param options         The options to update for the card
   * @param shouldBroadcast Whether or not to broadcast the update
   */
  updateSectionCard(id, url, options, shouldBroadcast) {
    if (this.sections.has(id)) {
      const card = this.sections.get(id).rows.find(elem => elem.url === url);
      if (card) {
        Object.assign(card, options);
      }
      this.emit(this.UPDATE_SECTION_CARD, id, url, options, shouldBroadcast);
    }
  },
  removeSectionCard(sectionId, url) {
    if (!this.sections.has(sectionId)) {
      return;
    }
    const rows = this.sections.get(sectionId).rows.filter(row => row.url !== url);
    this.updateSection(sectionId, {rows}, true);
  },
  onceInitialized(callback) {
    if (this.initialized) {
      callback();
    } else {
      this.once(this.INIT, callback);
    }
  },
  uninit() {
    Object.keys(this.CONTEXT_MENU_PREFS).forEach(k =>
      Services.prefs.removeObserver(this.CONTEXT_MENU_PREFS[k], this));
    SectionsManager.initialized = false;
  },
};

for (const action of [
  "ACTION_DISPATCHED",
  "ADD_SECTION",
  "REMOVE_SECTION",
  "ENABLE_SECTION",
  "DISABLE_SECTION",
  "UPDATE_SECTION",
  "UPDATE_SECTION_CARD",
  "INIT",
  "UNINIT",
]) {
  SectionsManager[action] = action;
}

EventEmitter.decorate(SectionsManager);

class SectionsFeed {
  constructor() {
    this.init = this.init.bind(this);
    this.onAddSection = this.onAddSection.bind(this);
    this.onRemoveSection = this.onRemoveSection.bind(this);
    this.onUpdateSection = this.onUpdateSection.bind(this);
    this.onUpdateSectionCard = this.onUpdateSectionCard.bind(this);
  }

  init() {
    SectionsManager.on(SectionsManager.ADD_SECTION, this.onAddSection);
    SectionsManager.on(SectionsManager.REMOVE_SECTION, this.onRemoveSection);
    SectionsManager.on(SectionsManager.UPDATE_SECTION, this.onUpdateSection);
    SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, this.onUpdateSectionCard);
    // Catch any sections that have already been added
    SectionsManager.sections.forEach((section, id) =>
      this.onAddSection(SectionsManager.ADD_SECTION, id, section));
  }

  uninit() {
    SectionsManager.uninit();
    SectionsManager.emit(SectionsManager.UNINIT);
    SectionsManager.off(SectionsManager.ADD_SECTION, this.onAddSection);
    SectionsManager.off(SectionsManager.REMOVE_SECTION, this.onRemoveSection);
    SectionsManager.off(SectionsManager.UPDATE_SECTION, this.onUpdateSection);
    SectionsManager.off(SectionsManager.UPDATE_SECTION_CARD, this.onUpdateSectionCard);
  }

  onAddSection(event, id, options) {
    if (options) {
      this.store.dispatch(ac.BroadcastToContent({type: at.SECTION_REGISTER, data: Object.assign({id}, options)}));

      // Make sure the section is in sectionOrder pref. Otherwise, prepend it.
      const orderedSections = this.orderedSectionIds;
      if (!orderedSections.includes(id)) {
        orderedSections.unshift(id);
        this.store.dispatch(ac.SetPref("sectionOrder", orderedSections.join(",")));
      }
    }
  }

  onRemoveSection(event, id) {
    this.store.dispatch(ac.BroadcastToContent({type: at.SECTION_DEREGISTER, data: id}));
  }

  onUpdateSection(event, id, options, shouldBroadcast = false) {
    if (options) {
      const action = {type: at.SECTION_UPDATE, data: Object.assign(options, {id})};
      this.store.dispatch(shouldBroadcast ? ac.BroadcastToContent(action) : ac.AlsoToPreloaded(action));
    }
  }

  onUpdateSectionCard(event, id, url, options, shouldBroadcast = false) {
    if (options) {
      const action = {type: at.SECTION_UPDATE_CARD, data: {id, url, options}};
      this.store.dispatch(shouldBroadcast ? ac.BroadcastToContent(action) : ac.AlsoToPreloaded(action));
    }
  }

  get orderedSectionIds() {
    return this.store.getState().Prefs.values.sectionOrder.split(",");
  }

  get enabledSectionIds() {
    let sections = this.store.getState().Sections.filter(section => section.enabled).map(s => s.id);
    // Top Sites is a special case. Append if the feed is enabled.
    if (this.store.getState().Prefs.values["feeds.topsites"]) {
      sections.push("topsites");
    }
    return sections;
  }

  moveSection(id, direction) {
    const orderedSections = this.orderedSectionIds;
    const enabledSections = this.enabledSectionIds;
    let index = orderedSections.indexOf(id);
    orderedSections.splice(index, 1);
    if (direction > 0) {
      // "Move Down"
      while (index < orderedSections.length) {
        // If the section at the index is enabled/visible, insert moved section after.
        // Otherwise, move on to the next spot and check it.
        if (enabledSections.includes(orderedSections[index++])) {
          break;
        }
      }
    } else {
      // "Move Up"
      while (index > 0) {
        // If the section at the previous index is enabled/visible, insert moved section there.
        // Otherwise, move on to the previous spot and check it.
        index--;
        if (enabledSections.includes(orderedSections[index])) {
          break;
        }
      }
    }

    orderedSections.splice(index, 0, id);
    this.store.dispatch(ac.SetPref("sectionOrder", orderedSections.join(",")));
  }

  async onAction(action) {
    switch (action.type) {
      case at.INIT:
        SectionsManager.onceInitialized(this.init);
        break;
      // Wait for pref values, as some sections have options stored in prefs
      case at.PREFS_INITIAL_VALUES:
        SectionsManager.init(action.data, this.store.dbStorage.getDbTable("sectionPrefs"));
        break;
      case at.PREF_CHANGED: {
        if (action.data) {
          const matched = action.data.name.match(/^(feeds.section.(\S+)).options$/i);
          if (matched) {
            await SectionsManager.addBuiltInSection(matched[1], action.data.value);
            this.store.dispatch({type: at.SECTION_OPTIONS_CHANGED, data: matched[2]});
          }
        }
        break;
      }
      case at.UPDATE_SECTION_PREFS:
        SectionsManager.updateSectionPrefs(action.data.id, action.data.value);
        break;
      case at.PLACES_BOOKMARK_ADDED:
        SectionsManager.updateBookmarkMetadata(action.data);
        break;
      case at.WEBEXT_DISMISS:
        if (action.data) {
          SectionsManager.removeSectionCard(action.data.source, action.data.url);
        }
        break;
      case at.SECTION_DISABLE:
        SectionsManager.disableSection(action.data);
        break;
      case at.SECTION_ENABLE:
        SectionsManager.enableSection(action.data);
        break;
      case at.SECTION_MOVE:
        this.moveSection(action.data.id, action.data.direction);
        break;
      case at.UNINIT:
        this.uninit();
        break;
    }
    if (SectionsManager.ACTIONS_TO_PROXY.includes(action.type) && SectionsManager.sections.size > 0) {
      SectionsManager.emit(SectionsManager.ACTION_DISPATCHED, action.type, action.data);
    }
  }
}

this.SectionsFeed = SectionsFeed;
this.SectionsManager = SectionsManager;
const EXPORTED_SYMBOLS = ["SectionsFeed", "SectionsManager"];
